package com.magenta.maxoptra.geocoding.osrm;

import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.StringUtils;
import com.magenta.maxoptra.gis.utils.*;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.util.DefaultClient;
import com.magenta.mrs.util.GeoInformationClient;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SchemeSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.*;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import static com.magenta.maxoptra.gis.GeoStatusCode.*;
import static com.magenta.maxoptra.gis.GisParameter.*;

public class GeocodingOsrmImpl extends BaseGeocoding<String> implements IGeoRouting {

    protected java.net.Proxy proxy;


    private static Map<String, GeoStatusCode> osrmErrors = new ConcurrentHashMap<String, GeoStatusCode>();

    static {
//        osrmErrors.put("10101", G_GEO_ABORTED_REQUEST);
//        osrmErrors.put("400", G_GEO_BAD_REQUEST);
//        osrmErrors.put("500", G_GEO_SERVER_ERROR);
//        osrmErrors.put("601", G_GEO_MISSING_QUERY);
//        osrmErrors.put("602", G_GEO_UNKNOWN_ADDRESS);
//        osrmErrors.put("603", G_GEO_UNAVAILABLE_ADDRESS);
        osrmErrors.put("0", G_GEO_SUCCESS);
        osrmErrors.put("207", G_GEO_UNKNOWN_DIRECTIONS);
//        osrmErrors.put("610", G_GEO_BAD_KEY);
//        osrmErrors.put("620", G_GEO_TOO_MANY_QUERIES);
//        osrmErrors.put("600", G_GEO_BAD_START_POINT);
    }

    public Map<String, GeoStatusCode> getProviderErrorMap() {
        return osrmErrors;
    }

    public String getProviderName() {
        return MapProviderType.osrm.name();
    }

    @Override
    protected boolean accuracyIsAccurateEnough(String accuracyThreshold, String accuracy) {
        return false;
    }

    @Override
    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit, List<String> accuracies) throws GeoException {
        return findAddressByKeyword(settings, keyword, resultAddressLimit);
    }

    @Override
    public void setClient(GeoInformationClient client) {
        this.client = client;
        HttpHost httpProxy = (HttpHost) client.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
        if (httpProxy != null) {
            String proxyHost = httpProxy.getHostName();
            int proxyPort = httpProxy.getPort();
            final Credentials credentials = ((DefaultHttpClient) client).getCredentialsProvider().getCredentials(
                    new AuthScope(proxyHost, proxyPort));

            SocketAddress socketAddress = new InetSocketAddress(proxyHost, proxyPort);
            this.proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddress);

            Authenticator.setDefault(new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    PasswordAuthentication p = new PasswordAuthentication(credentials.getUserPrincipal().getName(),
                            credentials.getPassword().toCharArray());
                    return p;
                }
            });
        }
    }

    private String getRoutingURL() {
        return getGisStringProperty(routing_url);
    }

    private String getRoutingPathURL() {
        return getGisStringProperty(routing_path_url);
    }

    private String getRoutingMatrixURL() {
        return getGisStringProperty(routing_matrix_url);
    }

    private String getPointLocatorURL() {
        return getGisStringProperty(point_locator_url);
    }

    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings,
                                               GeoAddress address, int resultAddressLimit) throws GeoException {
        throw new GeoException(new UnsupportedOperationException());
    }

    @Override
    public GeoRoute getRoute(GeographicCoordinates from,
                             GeographicCoordinates to) throws GeoException,
            ServerUnavailableException {
        return getRoute(from, to, null);
    }


    private GeographicCoordinates getNearestPoint(GeographicCoordinates sourcePoint, int zoomLevel) throws GeoException {
        String requestResult = doLocateRequest(sourcePoint, zoomLevel);
        //Parse request result (JSON).
        try {
            JSONObject o = new JSONObject(requestResult);
            if (o.has("status")) {
                String statusCodeString = o.getString("status");
                Integer status = safetyParseAsInteger(statusCodeString);
                if (status == null) {
                    status = G_GEO_UNKOWN_STATUS.getCode();
                }
                if (status != 0) {
                    return null;
                }
            }
            if (o.has("mapped_coordinate")) {
                JSONArray mappedCoordinate = o.getJSONArray("mapped_coordinate");
                if (mappedCoordinate.length() == 2) {
                    double latitude = mappedCoordinate.getDouble(0);
                    double longitude = mappedCoordinate.getDouble(1);
                    return new RoutingCoordinate(latitude, longitude);
                } else {
                    return null;
                }
            }
        } catch (JSONException e) {
        }
        return null;
    }

    public GeographicCoordinates getNearestRoutePoint(GeographicCoordinates sourcePoint) throws GeoException {
        if (client == null) {
            return null;
        }
        int zoomLevel = 18;
        int MIN_ZOOM_LEVEL = 10;
        GeographicCoordinates nearestPoint = null;
        while (zoomLevel > MIN_ZOOM_LEVEL && nearestPoint == null) {
            nearestPoint = getNearestPoint(sourcePoint, zoomLevel);
            zoomLevel--;
        }
        return nearestPoint;
    }


    @Override
    public GeoRoute getRoute(GeographicCoordinates a,
                             GeographicCoordinates b, VehicleType vehicleType)
            throws GeoException, ServerUnavailableException {
        if (client == null || a == null || b == null) {
            return null;
        }

        try {
            return tryToFindRoute(a, b);
        } catch (GeoException ignored) {
        }
        GeographicCoordinates neatestToA = getNearestRoutePoint(a);
        if (neatestToA != null && !neatestToA.equals(a)) {
            try {
                return tryToFindRoute(neatestToA, b);
            } catch (GeoException ignored) {
            }
        }
        GeographicCoordinates neatestToB = getNearestRoutePoint(b);
        if (neatestToB != null && !neatestToB.equals(b)) {
            try {
                return tryToFindRoute(a, neatestToB);
            } catch (GeoException ignored) {
            }
        }

        if (neatestToA != null && neatestToB != null) {
            try {
                return tryToFindRoute(neatestToA, neatestToB);
            } catch (GeoException ignored) {
            }
        }

        return new GeoRoute(G_GEO_RESULT_IS_NULL.getCode(), G_GEO_RESULT_IS_NULL.getCode(), a, b);
    }

    private GeoRoute tryToFindRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException {
        GetRouteProfiler profiler = new GetRouteProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), from, to);
        String requestResult = doRouteRequest(from, to);
        //Parse request result (JSON).
        try {
            JSONObject o = new JSONObject(requestResult);
            GeoStatusCode errorCode = G_GEO_UNKOWN_STATUS;
            if (o.has("status")) {
                String statusCodeString = o.getString("status");
                if (StringUtils.isNotEmpty(statusCodeString)) {
                    errorCode = osrmErrors.get(statusCodeString);
                }
                if (errorCode == null) {
                    errorCode = G_GEO_UNKOWN_STATUS;
                } else {
                    if (errorCode != G_GEO_SUCCESS) {
                        profiler.logDuration("trying to find");
                        throw new GeoException(errorCode);
                    }
                }
            }
            if (!o.has("route_summary")) {
                throw new GeoException(errorCode);
            }
            Object summary = o.get("route_summary");
            if (summary instanceof JSONObject) {
                int meters = ((JSONObject) summary).getInt("total_distance");
                int seconds = ((JSONObject) summary).getInt("total_time");
                Double dist = UnitsConverter.convert(LineUnits.METER, LineUnits.KILOMETRE, (double) meters);
                Long dur = UnitsConverter.convert(TimeUnits.SECOND, TimeUnits.MILLI, (long) seconds);
                GeoRoute geoRoute = new GeoRoute(dist, dur, from, to);
                profiler.logDuration(geoRoute);
                return geoRoute;
            }
        } catch (JSONException e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e.toString());
        }
        throw new GeoException(G_GEO_RESULT_IS_NULL);
    }

    private String doRouteRequest(GeographicCoordinates from, GeographicCoordinates to) throws GeoException {
        String request = MessageFormat.format(getRoutingURL(), Double.toString(from.getLatitude()), Double.toString(from.getLongitude()), Double.toString(to.getLatitude()), Double.toString(to.getLongitude()));
        logger.trace("REQUEST TO OSRM: %s", request);
        return doRequest(request);
    }

    private String doLocateRequest(GeographicCoordinates sourcePoint, int zoomLevel) throws GeoException {
        String requestResult;
        HttpResponse response = null;
        try {
            String request = MessageFormat.format(getPointLocatorURL(), Double.toString(sourcePoint.getLatitude()), Double.toString(sourcePoint.getLongitude()), zoomLevel);
            logger.trace("REQUEST TO OSRM: %s", request);
            String encodedUrl = URI.create(request).toString();
            HttpGet get = new HttpGet(encodedUrl);
            response = client.execute(get);
            requestResult = EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            throw new GeoException(e.toString());
        } finally {
            if (response != null) {
            }
        }
        return requestResult;
    }

    public static GeoInformationClient createHttpClient() {
        GeoInformationClient geoClient;
        HttpParams params = new BasicHttpParams();
        SchemeRegistry reg = new SchemeRegistry();
        SchemeSocketFactory socketFactory = new PlainSocketFactory();
        Scheme httpScheme = new Scheme("http", 80, socketFactory);
        reg.register(httpScheme);
        ClientConnectionManager connManager = new ThreadSafeClientConnManager(reg);
        geoClient = new DefaultClient(connManager, params);
        return geoClient;
    }

    private String doRequest(String request) throws GeoException {
        String requestResult = null;
        HttpResponse response = null;
        try {
            HttpClient client = createHttpClient();
            logger.trace("Request to matrix service: %s", request);
            URI uri = URI.create(request);
            String encodedUrl = uri.toString();
            HttpGet get = new HttpGet(encodedUrl);
            response = client.execute(get);
            requestResult = EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            logger.warn("Matrix service answer error = " + e.getMessage() + "}");
            throw new GeoException(e.toString());
        }
        return requestResult;
    }

    private String doRouteMatrixRequest(GeographicCoordinates from, List<? extends GeographicCoordinates> to) throws GeoException {
        String requestResult;
        HttpResponse response = null;
        try {
            String request = getRoutingMatrixURL() + "org=" + Double.toString(from.getLatitude()) + "," + Double.toString(from.getLongitude());
            for (GeographicCoordinates coordinates : to) {
                request = request + "&dst=" + Double.toString(coordinates.getLatitude()) + "," + Double.toString(coordinates.getLongitude());
            }
            logger.trace("REQUEST TO OSRM: %s", request);
            String encodedUrl = URI.create(request).toString();
            requestResult = doRequest(encodedUrl);
        } finally {
            if (response != null) {
            }
        }
        return requestResult;
    }

    private String doRoutePathRequest(GeographicCoordinates from, GeographicCoordinates to) throws GeoException {
        String requestResult;
        HttpResponse response = null;
        try {
            String request = MessageFormat.format(getRoutingPathURL(), Double.toString(from.getLatitude()), Double.toString(from.getLongitude()), Double.toString(to.getLatitude()), Double.toString(to.getLongitude()));
            logger.trace("REQUEST TO OSRM: %s", request);
            String encodedUrl = URI.create(request).toString();
            HttpGet get = new HttpGet(encodedUrl);
            response = client.execute(get);
            requestResult = EntityUtils.toString(response.getEntity());
        } catch (Exception e) {
            throw new GeoException(e.toString());
        } finally {
            if (response != null) {
            }
        }
        return requestResult;
    }


    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates)
            throws GeoException {
        throw new UnsupportedOperationException("");
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates,
                             VehicleType vehicleType) throws GeoException {
        throw new UnsupportedOperationException("");
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from,
                                   List<? extends GeographicCoordinates> to, int timeout)
            throws GeoException, ServerUnavailableException {

        GetRouteListByCoordsProfiler profiler = new GetRouteListByCoordsProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), from, to.size());
        List<GeoRoute> routes = new ArrayList<GeoRoute>();
        String requestResult = doRouteMatrixRequest(from, to);
        //Parse request result (JSON).
        try {
            JSONObject o = new JSONObject(requestResult);
//            String status = o.getString("status");
            if (o.has("matrix")) {
                JSONArray matrix = o.getJSONArray("matrix");
                if (matrix.length() > 0) {
                    for (int i = 0; i < matrix.length(); i++) {
                        JSONArray pair = (JSONArray) matrix.get(i);
                        if (pair != null && pair.length() == 2) {
                            int meters = (Integer) pair.get(0);
                            int seconds = (Integer) pair.get(1);
                            Double dist = (double) meters;
                            Long dur = (long) seconds;
                            if (meters > 0) {
                                dist = UnitsConverter.convert(LineUnits.METER, LineUnits.KILOMETRE, (double) meters);
                                dur = UnitsConverter.convert(TimeUnits.SECOND, TimeUnits.MILLI, (long) seconds);
                            }
                            GeoRoute geoRoute = new GeoRoute(dist, dur, from, to.get(i));
                            routes.add(geoRoute);
                        }
                    }
                }
                profiler.logDuration(routes.size());
                return routes;
            }
        } catch (JSONException e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e.toString());
        }
        return null;
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates) throws ServerUnavailableException {
        throw new UnsupportedOperationException("getCoordinateToCoordinateRoutes from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from,
                                   List<? extends GeographicCoordinates> to, VehicleType vehicleType,
                                   int timeout) throws GeoException, ServerUnavailableException {
        throw new UnsupportedOperationException("");
    }

     private List<GeoLocationRecord> decodePoly(String encoded) {
        List<GeoLocationRecord> poly = new ArrayList<GeoLocationRecord>();
        int index = 0, len = encoded.length();
        int lat = 0, lng = 0;
        while (index < len) {
            int b, shift = 0, result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lat += dlat;
            shift = 0;
            result = 0;
            do {
                b = encoded.charAt(index++) - 63;
                result |= (b & 0x1f) << shift;
                shift += 5;
            } while (b >= 0x20);
            int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
            lng += dlng;
            GeoLocationRecord p = new GeoLocationRecord(( lng  / 1000000.0), ( lat / 1000000.0));
            poly.add(p);
        }
        return poly;
    }

    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates a, GeographicCoordinates b) throws GeoException {
        if (client == null || a == null || b == null) {
            return null;
        }

        try {
            return tryToFindRoutePoints(a, b);
        } catch (GeoException ignored) {
        }
        GeographicCoordinates neatestToA = getNearestRoutePoint(a);
        if (neatestToA != null && !neatestToA.equals(a)) {
            try {
                List<GeoLocationRecord> list = tryToFindRoutePoints(neatestToA, b);
                if (list != null && list.size() > 1) {
                    LinkedList resultList = new LinkedList(list);
                    resultList.addFirst(new GeoLocationRecord(a.getLongitude(), a.getLatitude()));
                    return list;
                }
            } catch (GeoException ignored) {
            }
        }
        GeographicCoordinates neatestToB = getNearestRoutePoint(b);
        if (neatestToB != null && !neatestToB.equals(b)) {
            try {
                List<GeoLocationRecord> list = tryToFindRoutePoints(a, neatestToB);
                if (list != null && list.size() > 1) {
                    LinkedList resultList = new LinkedList(list);
                    resultList.addLast(new GeoLocationRecord(b.getLongitude(), b.getLatitude()));
                    return list;
                }
            } catch (GeoException ignored) {
            }
        }

        if (neatestToA != null && neatestToB != null) {
            try {
                List<GeoLocationRecord> list = tryToFindRoutePoints(neatestToA, neatestToB);
                if (list != null && list.size() > 1) {
                    LinkedList resultList = new LinkedList(list);
                    resultList.addFirst(new GeoLocationRecord(a.getLongitude(), a.getLatitude()));
                    resultList.addLast(new GeoLocationRecord(b.getLongitude(), b.getLatitude()));
                    return list;
                }
            } catch (GeoException ignored) {
            }
        }

        return null;
    }

    private List<GeoLocationRecord> tryToFindRoutePoints(GeographicCoordinates a, GeographicCoordinates b) throws GeoException {
        GetRoutePointsByCoordsProfiler profiler = new GetRoutePointsByCoordsProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), a, b);
        String requestResult = doRoutePathRequest(a, b);
        //Parse request result (JSON).
        try {
            JSONObject o = new JSONObject(requestResult);

            GeoStatusCode errorCode = G_GEO_UNKOWN_STATUS;
            if (o.has("status")) {
                String statusCodeString = o.getString("status");
                if (StringUtils.isNotEmpty(statusCodeString)) {
                    errorCode = osrmErrors.get(statusCodeString);
                }
                if (errorCode == null) {
                    errorCode = G_GEO_UNKOWN_STATUS;
                } else {
                    if (errorCode != G_GEO_SUCCESS) {
                        profiler.logDuration("trying to find");
                        throw new GeoException(errorCode);
                    }
                }
            }

            if (!o.has("route_geometry")) {
                throw new GeoException(errorCode);
            }
            String routeGeometry = o.getString("route_geometry");
            List<GeoLocationRecord> geoLocationRecords = decodePoly(routeGeometry);
            profiler.logDuration(geoLocationRecords.size());
            return geoLocationRecords;
        } catch (JSONException e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e.toString());
        }
    }

    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings,
                                                  List<GeographicCoordinates> points) throws GeoException {
        throw new UnsupportedOperationException("");
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from,
                                    List<? extends GeographicCoordinates> to, int timeout)
            throws GeoException, ServerUnavailableException {
        throw new UnsupportedOperationException("");
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from,
                                    List<? extends GeographicCoordinates> to, VehicleType vehicleType,
                                    int timeout) throws GeoException, ServerUnavailableException {
        throw new UnsupportedOperationException("");
    }

    private static final Pattern DECIMAL_INTEGER = Pattern.compile("-?\\d+");

    public static Integer safetyParseAsInteger(String text) {
        if (text != null && (DECIMAL_INTEGER.matcher(text.trim())).matches()) {
            return Integer.valueOf(text.trim());
        }
        return null;
    }
}

